ltaoo's web

Async 使用笔记

之前虽然有过关于async使用的笔记,但是真正在项目中使用时,发现还是存在一些问题,所以重新对async进行更深入的学习。

顺序执行

顺序执行是基本功能,因为async引入就是因为异步代码要顺序执行会非常麻烦,无论是callback还是promise都不是很直观,而使用async能够写异步代码看起来像同步一样。

不过我比较迷惑的是,在循环中,如何控制代码按顺序执行。假设有这么一个需求:

存在一个数组,保存了很多笔记对象,遍历该数组发送异步请求创建云端笔记,在发送请求前,需要先判断该笔记所属的笔记本是否存在,如果不存在则先创建笔记本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const noteAry = [{
title: 'note1',
notebook: 1
}, {
title: 'note2',
notebook: 1
}, {
title: 'note3',
notebook: 2
}]
// 逻辑
noteAry.forEach(note => {
if(/*如果该笔记所属的笔记本没有创建*/) {
// 就创建笔记本
// 笔记本创建完成后,创建笔记
}
})

笔记的创建是依赖于笔记本存在的,所以如果笔记本不存在,就一定要先创建笔记本,并在笔记本创建成功的回调中创建笔记。那问题来了,创建笔记的函数是不是需要调用两次?类似这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// 请求库
const fetch = require('isomorphic-fetch')
// 将对象作为数据库处理的一个库
const lowdb = require('lowdb')

// 同步判断笔记本是否存在
function existsSync(id) {
const db = lowdb('./db.json')
return db.get('notebooks').find({id}).value()
}

// 创建笔记本函数
function createNotebook(notebook) {
return new Promise((resolve, reject) => {
fetch('http://127.0.0.1:3000/notebooks', {
method: 'POST',
body: JSON.stringify(notebook)
})
.then(res => res.json())
.then(json => {
resolve(json)
})
.catch(err => {
reject(err)
})
})
}

// 创建笔记函数
function createNote(note) {
return new Promise((resolve, reject) => {
fetch('http://127.0.0.1:3000/notes', {
method: 'POST',
body: JSON.stringify(note)
})
.then(res => res.json())
.then(json => {
resolve(json)
})
.catch(err => {
reject(err)
})
})
}

const noteAry = [{
title: 'note1',
notebook: 1
}, {
title: 'note2',
notebook: 1
}, {
title: 'note3',
notebook: 2
}]

// 先使用 promise
noteAry.forEach(note => {
if(!existsSync(note.notebook)) {
console.log(`笔记本${note.notebook}不存在,先创建笔记本`)
// 如果笔记本不存在
createNotebook(note.notebook)
.then(res => {
console.log(`笔记本${note.notebook}创建成功`)
return createNote(note)
})
.then(res => {
console.log(`笔记${note.title}创建成功`)
})
.catch(err => {
console.log(err)
})
} else {
console.log(`笔记本${note.notebook}已经存在,直接创建笔记`)
createNote(note)
.then(res => {
console.log(`笔记${note.title}创建成功`)
})
.catch(err => {
console.log(err)
})
}
})

打印的结果是:

1
2
3
4
5
6
7
8
9
笔记本1不存在,先创建笔记本
笔记本1不存在,先创建笔记本
笔记本2不存在,先创建笔记本
笔记本1创建成功
笔记本1创建成功
笔记本2创建成功
笔记note1创建成功
笔记note2创建成功
笔记note3创建成功

但是希望能够打印:

1
2
3
4
5
6
7
8
笔记本1不存在,先创建笔记本
笔记本1创建成功
笔记note1创建成功
笔记本1已经存在,直接创建笔记
笔记note2创建成功
笔记本2不存在,先创建笔记本
笔记本note3创建成功
笔记创建成功

而且数据库文件也存在三个笔记本数据。没有成功的原因很简单,请求还未结束,就已经开始了下一次的循环,这时候数据库中还未写入新的笔记本,所以判断笔记本1还不存在

先解决如何实现我们的预期需求。
引入一个临时变量promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 先使用 promise 
function realCreateNote(note) {
return new Promise((resolve, reject) => {
if(!existsSync(note.notebook)) {
console.log(`笔记本${note.notebook}不存在,先创建笔记本`)
// 如果笔记本不存在
createNotebook(note.notebook)
.then(res => {
console.log(`笔记本${note.notebook}创建成功`)
return createNote(note)
})
.then(res => {
console.log(`笔记${note.title}创建成功`)
resolve(note.title)
})
.catch(err => {
console.log(err)
reject(err)
})
} else {
console.log(`笔记本${note.notebook}已经存在,直接创建笔记`)
createNote(note)
.then(res => {
console.log(`笔记${note.title}创建成功`)
resolve(note.title)
})
.catch(err => {
console.log(err)
reject(err)
})
}
})
}

function main() {
// 临时变量
let promise = Promise.resolve()
noteAry.forEach(note => {
promise = promise.then(() => realCreateNote(note))
})

return promise
}

main().then(() => {
console.log('笔记均创建完成')
}).catch(err => {
console.error(err)
})

能够实现预期的需求。

看起来还好,但是存在一个“复制代码”的问题,如果要修改笔记创建成功的log,却需要同时修改两处,如果还需要在笔记创建成功后进行处理,就要复制两份代码,肯定不好,但是却不能写成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function realCreateNote(note) {
return new Promise((resolve, reject) => {
if(!existsSync(note.notebook)) {
console.log(`笔记本${note.notebook}不存在,先创建笔记本`)
// 如果笔记本不存在
createNotebook(note.notebook)
.then(res => {
console.log(`笔记本${note.notebook}创建成功`)
})
.catch(err => {
console.log(err)
reject(err)
})
}
console.log(`笔记本${note.notebook}已经存在,直接创建笔记`)
createNote(note)
.then(res => {
console.log(`笔记${note.title}创建成功`)
resolve(note.title)
})
.catch(err => {
console.log(err)
reject(err)
})
})
}

因为如果笔记不存在,createNote函数是必须在createNotebook函数的成功回调中,这样写很可能在笔记本还未创建成功时,就发送请求创建笔记,从打印的信息也能够看出问题所在:

1
2
3
4
5
6
7
8
9
10
11
笔记本1不存在,先创建笔记本
笔记本1已经存在,直接创建笔记
笔记本1创建成功
笔记note1创建成功
笔记本1已经存在,直接创建笔记
笔记note2创建成功
笔记本2不存在,先创建笔记本
笔记本2已经存在,直接创建笔记
笔记本2创建成功
笔记note3创建成功
笔记均创建完成

没有想到使用promise解决的方案。async`是否能够很好的处理这种情况?

async

由于async能够暂停函数的执行,所以使用await返回的异步函数,肯定是已经请求结束的,所以如果使用async,上面的代码可以改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 使用 async
async function realCreateNote(note) {
if(!existsSync(note.notebook)) {
console.log(`笔记本${note.notebook}不存在,先创建笔记本`)
// 如果笔记本不存在
try {
await createNotebook(note.notebook)
console.log(`笔记本${note.notebook}创建成功`)
}catch(err) {
console.log(err)
}
}
console.log(`笔记本${note.notebook}已经存在,直接创建笔记`)
try {
await createNote(note)
console.log(`笔记${note.title}创建成功`)
}catch(err) {
console.log(err)
}
}

realCreateNote(noteAry[0])

直接调用该函数,能够正确打印出预期的信息:

1
2
3
4
笔记本1不存在,先创建笔记本
笔记本1创建成功
笔记本1已经存在,直接创建笔记
笔记note1创建成功

表示的确是按照顺序执行,再次调用也是没问题realCreateNote(noteAry[1])

1
2
笔记本1已经存在,直接创建笔记
笔记note2创建成功

而在forEach循环中却并没有按照预期执行:

1
2
3
noteAry.forEach(note => {
realCreateNote(note)
})

1
2
3
4
5
6
7
8
9
10
11
12
笔记本1不存在,先创建笔记本
笔记本1不存在,先创建笔记本
笔记本2不存在,先创建笔记本
笔记本1创建成功
笔记本1已经存在,直接创建笔记
笔记本1创建成功
笔记本1已经存在,直接创建笔记
笔记本2创建成功
笔记本2已经存在,直接创建笔记
笔记note1创建成功
笔记note2创建成功
笔记note3创建成功

即使将realCreateNote函数返回promise实例,在循环内使用await关键字也不行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
async function realCreateNote(note) {
if(!existsSync(note.notebook)) {
console.log(`笔记本${note.notebook}不存在,先创建笔记本`)
// 如果笔记本不存在
try {
await createNotebook(note.notebook)
console.log(`笔记本${note.notebook}创建成功`)
}catch(err) {
console.log(err)
return Promise.reject(err)
}
}
console.log(`笔记本${note.notebook}已经存在,直接创建笔记`)
try {
await createNote(note)
console.log(`笔记${note.title}创建成功`)
return Promise.resolve(`笔记${note.title}创建成功`)
}catch(err) {
console.log(err)
return Promise.reject(err)
}
}

// realCreateNote(noteAry[1])

noteAry.forEach(async function (note) {
try {
await realCreateNote(note)
}catch(err) {
console.log(err)
}
})

但是改成for循环后就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 使用 async
async function realCreateNote(note) {
if(!existsSync(note.notebook)) {
console.log(`笔记本${note.notebook}不存在,先创建笔记本`)
// 如果笔记本不存在
try {
await createNotebook(note.notebook)
console.log(`笔记本${note.notebook}创建成功`)
}catch(err) {
console.log(err)
return Promise.reject(err)
}
}
console.log(`笔记本${note.notebook}已经存在,直接创建笔记`)
try {
await createNote(note)
console.log(`笔记${note.title}创建成功`)
return Promise.resolve(`笔记${note.title}创建成功`)
}catch(err) {
console.log(err)
return Promise.reject(err)
}
}
async function main() {
for(let i = 0, len = noteAry.length; i < len; i++) {
const note = noteAry[i]
try {
await realCreateNote(note)
}catch(err) {
console.log(err)
}
}
}
main()

这里之所以要添加main函数,是因为await关键字只能在async函数内使用。

打印的信息为:

1
2
3
4
5
6
7
8
9
10
笔记本1不存在,先创建笔记本
笔记本1创建成功
笔记本1已经存在,直接创建笔记
笔记note1创建成功
笔记本1已经存在,直接创建笔记
笔记note2创建成功
笔记本2不存在,先创建笔记本
笔记本2创建成功
笔记本2已经存在,直接创建笔记
笔记note3创建成功

总结

虽然使用Promiseasync实现了相同的功能,但async在代码逻辑上和思维是一致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
async function main() {
for(let i = 0, len = noteAry.length; i < len; i++) {
const note = noteAry[i]
if(!existsSync(note.notebook)) {
console.log(`笔记本${note.notebook}不存在,先创建笔记本`)
// 如果笔记本不存在
try {
await createNotebook(note.notebook)
console.log(`笔记本${note.notebook}创建成功`)
}catch(err) {
console.log(err)
}
}
console.log(`笔记本${note.notebook}已经存在,直接创建笔记`)
try {
await createNote(note)
console.log(`笔记${note.title}创建成功`)
}catch(err) {
console.log(err)
}
}
}
main()

所以相比Promise而言要更好,而且解决了一个使用Promise无法解决的“复制代码”的问题。

参考